package com.fortune.cat.filter.factory;

import com.fortune.cat.bean.SignVerifyConfig;
import com.fortune.cat.manager.SignAccessManager;
import com.fortune.cat.utils.ServerResponseUtils;
import com.fortune.cat.utils.SignProcessorUtils;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.*;

import static com.fortune.cat.constant.ServerWebExchangeConstants.GATEWAY_REQUEST_BODY_STR;

/**
 * @author zhaozeyang
 * @date 2019/4/26 17:13
 * @description
 */
@Slf4j
@Component
public class DefaultSignVerifyFilterFactory extends AbstractCustomizeGatewayFilterFactory<SignVerifyConfig> {


    private final static String ACCESSID = "accessid";

    private final static Gson GSON = new Gson();

    private final SignAccessManager signAccessManager;

    private static String ERROR_RETURN_STR;

    static {
        Map<String, Object> map = new HashMap<>();
        map.put("msg", "访问异常，请确认手机系统时间准确或重新登录后再试");
        map.put("status", 400403);
        ERROR_RETURN_STR = GSON.toJson(map);
    }

    private final static List<String> SignProcessor = new ArrayList<String>() {{
        add("HmacMD5".toLowerCase());
        add("HmacSHA1".toLowerCase());
        add("HmacSHA256".toLowerCase());
    }};

    public DefaultSignVerifyFilterFactory(SignAccessManager signAccessManager) {
        super(SignVerifyConfig.class);
        this.signAccessManager = signAccessManager;
    }

    @Override
    protected Mono<Void> doApply(SignVerifyConfig config, ServerWebExchange exchange, GatewayFilterChain chain) {
        String sign = exchange.getRequest().getHeaders().getFirst("sign");
        ServerHttpRequest request = exchange.getRequest();
        log.info("DefaultSignVerifyFilterFactory | verify sign start, path is {}, sign is {}", exchange.getRequest().getPath(), sign);
        //开始签名校验
        if (StringUtils.isBlank(request.getHeaders().getFirst(ACCESSID))) {
            log.info("DefaultSignVerifyFilterFactory | not accessId URI : {}", request.getURI());
            if (config.isHalfOpen()) {
                return chain.filter(exchange);
            } else {
                return ServerResponseUtils.returnCommonResponse(ERROR_RETURN_STR, exchange, HttpStatus.BAD_REQUEST);
            }
        }
        String timestamp = request.getHeaders().getFirst("timestamp");
        //超时 不接受请求
        if (StringUtils.isBlank(timestamp) || System.currentTimeMillis() - Long.valueOf(timestamp) > config.getSignTimeOutInMillisecond()) {
            log.info("DefaultSignVerifyFilterFactory | invalid timestamp : {}", timestamp);
            return ServerResponseUtils.returnCommonResponse(ERROR_RETURN_STR, exchange, HttpStatus.BAD_REQUEST);
        }
        //获取参数
        try {
            return getSignKVMapping(exchange, config).flatMap(p -> readRequestJson2Map(p, exchange))
                    .flatMap(map -> {
                        if (!validate(request, map)) {
                            log.warn("DefaultSignVerifyFilterFactory | sign Fail");
                            return ServerResponseUtils.returnCommonResponse(ERROR_RETURN_STR, exchange, HttpStatus.BAD_REQUEST);
                        } else {
                            return chain.filter(exchange);
                        }
                    });
        } catch (Exception e) {
            log.info("DefaultSignVerifyFilterFactory | error", e);
            return ServerResponseUtils.returnCommonResponse(ERROR_RETURN_STR, exchange, HttpStatus.BAD_REQUEST);
        }
    }

    private boolean validate(ServerHttpRequest request, TreeMap<String, String> map) {
        String algorithm = request.getHeaders().getFirst("algorithm");
        log.info("validate | receive signMethod: {} , sign : {}", algorithm, request.getHeaders().get("sign"));
        if (StringUtils.isBlank(algorithm)) {
            return false;
        }
        if (SignProcessor.contains(algorithm.trim().toLowerCase())) {
            return false;
        }
        String privateKey = signAccessManager.getAccessKeyById(request.getHeaders().getFirst(ACCESSID));
        log.info("validate | will use privateKey : {}", privateKey);
        StringBuilder stringBuilder = new StringBuilder();
        for (Map.Entry<String, String> entry : map.entrySet()) {
            stringBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
        }
        String data = stringBuilder.length() > 0 ? stringBuilder.deleteCharAt(stringBuilder.length() - 1).toString() : stringBuilder.toString();
        log.info("validate | data is {}", data);
        String encrypt = SignProcessorUtils.encrypt(privateKey, data, algorithm);
        log.info("validate | encrypt result is : {}", encrypt);
        return encrypt.equals(Optional.ofNullable(request.getHeaders().getFirst("sign")).map(String::toUpperCase).orElse(null));
    }

    private Mono<TreeMap<String, String>> readRequestJson2Map(TreeMap<String, String> p, ServerWebExchange exchange) {
        //此出只取json格式
        MediaType contentType = exchange.getRequest().getHeaders().getContentType();
        if (!contentType.includes(MediaType.APPLICATION_JSON) &&
                !contentType.includes(MediaType.APPLICATION_JSON_UTF8)) {
            return Mono.just(p);
        }
        String body = exchange.getAttribute(GATEWAY_REQUEST_BODY_STR);
        if (StringUtils.isBlank(body)) {
            return Mono.just(p);
        }
        Map<String, String> map = GSON.fromJson(body, new TypeToken<Map<String, String>>() {
        }.getType());
        p.putAll(map);
        return Mono.just(p);
    }

    @Override
    protected int doGetOrder() {
        return 0;
    }

    @Override
    protected boolean skipPresent(SignVerifyConfig config, ServerWebExchange exchange) {
        return config.getSkipUrls().contains(exchange.getRequest().getPath().value());
    }

    /**
     * 解析 query，header，form data 参数
     *
     * @param exchange
     * @param config
     * @return
     * @throws Exception
     */
    private Mono<TreeMap<String, String>> getSignKVMapping(ServerWebExchange exchange, SignVerifyConfig config) throws Exception {
        ServerHttpRequest request = exchange.getRequest();
        String contentType = request.getHeaders().getFirst("Content-Type");
        return Mono.zip(Mono.just(request.getQueryParams()), Mono.just(request.getHeaders()), exchange.getFormData())
                .map(tuple -> {
                    TreeMap<String, String> result = new TreeMap<>();
                    tuple.getT1().forEach((key, values) -> addValue(result, key, values));
                    tuple.getT2().forEach((key, values) -> {
                        if (config.getSignHeaderNames().contains(key)) {
                            addHeaderValue(result, key, values);
                        }
                    });
                    if (StringUtils.isNotBlank(contentType)
                            && (contentType.startsWith(MediaType.MULTIPART_FORM_DATA_VALUE)
                            || contentType.startsWith(MediaType.APPLICATION_FORM_URLENCODED_VALUE))) {
                        tuple.getT3().forEach((key, values) -> addValue(result, key, values));
                    }
                    return result;
                });
    }

    private void addHeaderValue(TreeMap<String, String> result, String key, List<String> values) {
        if (values != null && values.size() > 0) {
            //此处还需判断 val[0是否为空]
            if (StringUtils.isNotBlank(values.get(0))) {
                result.put(key, values.get(0).trim());
            }
        }
    }

    private void addValue(TreeMap<String, String> result, String key, List<String> values) {
        if (values != null && values.size() > 0) {
            //此处还需判断 val[0是否为空]
            if (StringUtils.isNotBlank(values.get(0))) {
                result.put(key, values.get(0));
            }
        }
    }
}
